#!/usr/bin/perl

use RPC::XML ':all';
use RPC::XML::Client;
require LWP::ConnCache;
use MIME::Base64;
use Crypt::SSLeay;
use Getopt::Std;

my $VERSION="2.1";
$RPC::XML::ENCODING = "utf-8";

if ($#ARGV < 1) {
 if ( $#ARGV == 0 && $ARGV[0] eq "version") {
    printf("wgcmd.pl v%s\n",$VERSION);
    exit 0;
 }

 printf( "usage: %s [options] <system_ip_or_name> <command> <xpath_or_filename> ...\n", "wgcmd.pl");
 print "\n options are -u for user name, -p for password -s for sid instead of login call -t timeout in seconds -c for cluster operations\n";
 print "\n <command> is one from [version|status|action|upgrade|get|set|getfile|validate|backup|restore|cli]\n";
 print " <xpath>   is valid expath expression or a file name for upgrade command\n";
 print "\nFor example:\n";
 print "\n'wgcmd.pl -u status -p readonly 192.168.111.1 status /system/version'";
 print "\n      will return system version\n";
 print "\n'wgcmd.pl 192.168.111.1 action /system/reboot'";
 print "\n      will reboot firebox\n";
 print "\n'wgcmd.pl 192.168.111.1 upgrade /tmp/utm.sysa-dl'";
 print "\n      will upgrade firmware on appliance\n";
 exit;
}

# our call is array of requests
# each of them can have multiple members/parameters 
my @params=();
my $user='admin';
my $password='readwrite';
my $reboot=1;
my $cluster=0;
my $sid='';
my $image='';
my $timeout=30;
my %options=();

getopts("u:p:s:t:rc",\%options);
# like the shell getopt, "u:" means u takes an argument
if (defined $options{u}) {
    $user= $options{u};
} 
if (defined $options{p}) {
    $password= $options{p};
}
if (defined $options{s}) {
    $sid= $options{s};
} 
if (defined $options{t}) {
    $timeout= $options{t};
} 
if (defined  $options{r}) {
    $reboot=0;
}
if (defined  $options{c}) {
    $cluster=1;
}

if ( $ARGV[1] eq "version" ) {
    printf("wgcmd.pl: v%s\n",$VERSION);
    exit 0;
} elsif ( $ARGV[1] eq "getfile" ) {
    $method = "/agent/getConfigFile";
    if ($#ARGV == 2 ) {
        if ($ARGV[2] eq "compressed") {
            push (@params,{'compress' => "1"});
        } elsif ($ARGV[2] eq "uncompressed") {
            push (@params,{'compress' => "0"});
        } else {
            print "unkonw parameter: $ARGV[2]\n";
            exit;
        }
    }
} elsif ( $ARGV[1] eq "get" ) {
    if ($#ARGV < 2 ) {
        print "get command requires parameter\n";
        exit;
    }
    $method = "/agent/get";
    push (@params,{'xpath' => $ARGV[2]});
} elsif ( $ARGV[1] eq "status" ) {
    my %p={};

    if ($#ARGV < 2 ) {
        print "status command requires parameter\n";
        exit;
    }
    $method = "/agent/status";

    # this expects format like: XPATH key1=val1 key2=val2 XPATH
    for ($i = 2; $i <= $#ARGV; $i++) {
        if ( $ARGV[$i] =~ /=/ ) {
            # this looks like key=val pair
            # so just add it to last hash
            $p= $params[$#params];
            ($key,$val) = split(/=/, $ARGV[$i],2 );
            $p->{$key}=$val;
        } elsif (  $ARGV[$i] =~ /</ ) {
            ($key,$name) = split(/</, $ARGV[$i],2 );

            open(FILE, $name) or die "$name: $!";
            $file='';
            while (read(FILE, $buf, 60*57)) {
                $file.=$buf;
            }
            close(FILE);

            $bb= RPC::XML::base64->new($file,0);
            $p->{$key}=$bb; 
        } else {
            push (@params,{});    
            $p= $params[$#params];
            $p->{status}=$ARGV[$i];
        }
    };

} elsif ( $ARGV[1] eq "action" ) {
    my %p={};

    if ($#ARGV < 2 ) {
        print "status command requires parameter\n";
        exit;
    }
    $method = "/agent/action";

    push (@params,{});
    $p= $params[$#params];
    $p->{action}=$ARGV[2];
    $count++;

    for ($i = 3; $i <= $#ARGV; $i++) {
        if ( $ARGV[$i] =~ /=/ ) {
            # this looks like key=val pair
            # so just add it to last hash
            ($key,$val) = split(/=/, $ARGV[$i],2 );
            $p->{$key}=$val;
        } elsif (  $ARGV[$i] =~ /</ ) {
            ($key,$name) = split(/</, $ARGV[$i],2 );

            open(FILE, $name) or die "$name: $!";
            $file='';
            while (read(FILE, $buf, 60*57)) {
                $file.=$buf;
            }
            close(FILE);

            $bb= RPC::XML::base64->new($file,0);
            $p->{$key}=$bb; 
        } else {
            $p->{$ARGV[$i]}='';
        }   
    };      

} elsif ( $ARGV[1] eq "cli" ) {
    my %p={};

    if ($#ARGV < 2 ) {
        print "cli command requires parameter\n";
        exit;
    }
    $method = "cli";

    push (@params,{});
    $p= $params[$#params];
    
    for ($i = 2; $i <= $#ARGV; $i++) {
        if ( $ARGV[$i] =~ /=/ ) {
            # this looks like key=val pair
            # so just add it to last hash
            ($key,$val) = split(/=/, $ARGV[$i],2 );
            $p->{$key}=$val;
        } elsif (  $ARGV[$i] =~ /</ ) {
            ($key,$name) = split(/</, $ARGV[$i],2 );

            open(FILE, $name) or die "$name: $!";
            $file='';
            while (read(FILE, $buf, 60*57)) {
                $file.=$buf;
            }
            close(FILE);

            $bb= RPC::XML::base64->new($file,0);
            $p->{$key}=$bb;
        } else {
            $p->{$ARGV[$i]}='';
        }
    };
} elsif ( $ARGV[1] eq "upgrade" ) {
    if ($#ARGV < 2 ) {
        print "upgrade command requires parameter with file name\n";
        exit;
    }
    open(FILE, $ARGV[2]) or die "$!";
    while (<FILE>) {
        $image.=$_;
       #$image.=encode_base64($buf);
    }
    close(FILE);

    #push (@params,{'image'=>$image});
    if ($cluster) {
        $method = "/cluster/upgrade";
    } else {
        $method = "/agent/upgrade";
    }
} elsif ( $ARGV[1] eq "restore" ) {
    if ($#ARGV < 2 ) {
        print "restore command requires parameter with file name\n";
        exit;
    }
    open(FILE, $ARGV[2]) or die "$!";
    while (<FILE>) {
        $image.=$_;
    }
    close(FILE);

    if ( $cluster ) {
        $method = "/cluster/restore";
    } else {
        $method = "/agent/restore";
    }
    if ($#ARGV > 2 ) {
        ($key,$val) = split(/=/, $ARGV[3],2 );
        if ( $key ne "password" ) {
            print "only 'password' parameter expected\n";
            exit 1;
        }
        $method=$method."?password=".encode_base64( $val);
    }
} elsif ( $ARGV[1] eq "backup" ) {
    if ($#ARGV < 2 ) {
        print "backup command requires parameter with file name\n";
        exit;
    }
    open(FILE, "> $ARGV[2]") or die "$ARGV[2]: $!";

    if ( $cluster ) {
        $method = "/cluster/backup";
    } else {
        $method = "/agent/backup";
    }
    
    push (@params,{});
    $p= $params[$#params];
    $p->{action}=$method;
    $count++;

    for ($i = 3; $i <= $#ARGV; $i++) {
        if ( $ARGV[$i] =~ /=/ ) {
            # this looks like key=val pair
            # so just add it to last hash
            ($key,$val) = split(/=/, $ARGV[$i],2 );
            $p->{$key}=$val;
        } elsif (  $ARGV[$i] =~ /</ ) {
            ($key,$name) = split(/</, $ARGV[$i],2 );

            open(FILE, $name) or die "$name: $!";
            $file='';
            while (read(FILE, $buf, 60*57)) {
                $file.=$buf;
            }
            close(FILE);

            $bb= RPC::XML::base64->new($file,0);
            $p->{$key}=$bb;
        } else {
            $p->{$ARGV[$i]}='';
        }
    };
} elsif ( $ARGV[1] eq "set" ) {
    if ($#ARGV < 2 ) {
        print "set command requires parameter with file name\n";
        exit;
    }
    open(FILE, $ARGV[2]) or die "$!";
    $config='';
    while (<FILE>) {
       $image.=$_;
    }
    close(FILE);
    #push (@params,{'config'=>$image,'reboot'=>1});
    push (@params,{'config'=>$image});
    $method = "/agent/setConfig";

    $p= $params[$#params];
    for ($i = 3; $i <= $#ARGV; $i++) {
        if ( $ARGV[$i] =~ /=/ ) {
            # this looks like key=val pair
            # so just add it to last hash
            ($key,$val) = split(/=/, $ARGV[$i],2 );
            $p->{$key}=$val;
        } elsif (  $ARGV[$i] =~ /</ ) {
            ($key,$name) = split(/</, $ARGV[$i],2 );

            open(FILE, $name) or die "$name: $!";
            $file='';
            while (read(FILE, $buf, 60*57)) {
                $file.=$buf;
            }
            close(FILE);
    
            $bb= RPC::XML::base64->new($file,0);
            $p->{$key}=$bb;
        } else { 
            $p->{$ARGV[$i]}='';
        }
    };
} elsif ( $ARGV[1] eq "validate" ) {
    if ($#ARGV ne 2 ) {
        print "set command requires parameter with file name\n";
        exit;
    }
    open(FILE, $ARGV[2]) or die "$!";
    $config='';
    while (<FILE>) {
       $image.=$_;
    }
    close(FILE);
    #@params = smart_encode(RPC::XML::struct->new(config=>$image));
    push (@params,{'config'=>$image});
    $method = "validate";
} elsif ( $ARGV[1] eq "mgmt" ) {
    $method="/agent/mgmt";
    if ($#ARGV < 2 ) {
        print "mgmt command requires at least function name\n";
        exit;
    }

    # this expects format like: XPATH key1=val1 key2=val2 XPATH
    for ($i = 2; $i <= $#ARGV; $i++) {
        if ( $ARGV[$i] =~ /=/ ) {
            # this looks like key=val pair
            # so just add it to last hash
            $p= $params[$#params];
            ($key,$val) = split(/=/, $ARGV[$i],2 );
            $p->{$key}=$val;
        } elsif (  $ARGV[$i] =~ /</ ) {
            ($key,$name) = split(/</, $ARGV[$i],2 );

            open(FILE, $name) or die "$name: $!";
            $file='';
            while (read(FILE, $buf, 60*57)) {
                $file.=$buf;
            }
            close(FILE);

            $bb= RPC::XML::base64->new($file,0);
            $p->{$key}=$bb; 
        } else {
            push (@params,{});
            $p= $params[$#params];
            $p->{function}=$ARGV[$i];
        }
    };

} else {
    print "Unknown command\n";
    exit;
}

#process rest of arguments
if ( $ARGV[0] =~ /^http:/ ) {
    $host = $ARGV[0]; 
} elsif ( $ARGV[0] =~ /^https:/) {
    $host = $ARGV[0];
} else {
    $host = "https://" . $ARGV[0] .":4117";
}   
if ( (  $ARGV[1] eq "backup" || $ARGV[1] eq "upgrade" || $ARGV[1] eq "restore" ) && $timeout < 300 ) {
    # make sure we have decent timeout for slow operations 
    # FIXME this will increase timeout for login as well but it looks like
    #       https does not change on later $agent->timeout()
    #       only one way how to fix it would be create two objects in this case
    $timeout=300;
}


$fbx = RPC::XML::Client->new("$host/agent/login", error_handler => sub { die "error: $_[0]" } );
#$fbx = RPC::XML::Client->new("$host/agent/login");
# make sure we do persistent connection
# without connection cache LWP agent will close TCP after each 
# request
$agent=$fbx->useragent;
$agent->conn_cache(LWP::ConnCache->new( ));
$agent->cookie_jar( {} );
$agent->timeout($timeout);
$req = $fbx->request;
$req->header('Accept-encoding', '');

if ($sid ne '' ) {
    $req->header('Cookie',"sessionid=$sid");
} else {
    #$data = RPC::XML::struct->new(user=>$user,password=>$password,version=>1);
    $data = RPC::XML::struct->new(user=>$user,password=>$password);
    #$data = RPC::XML::struct->new(user=>"status",password=>"readonly",challenge=>"qdTUeUb/G5rbskXgs+mx63X8Oo3vybmdF9YxBkCFbXcr50TDTyJnFA==");

    $resp = $fbx->send_request('/agent/login',$data);

    if ( $resp->is_fault ) {
        printf ("%d error: %s\n",$resp->code,$resp->string);
        exit 1;
    }

    # try it as struct first 
    $sid = $resp->value->{sid};

    if (!defined $sid ) {
        # this is old style 
        $sid = $resp->value;
    }
}

if ($sid eq '' ) {
    die("error: unable to get session ID\n");
}

if ( (  $ARGV[1] eq "backup" || $ARGV[1] eq "upgrade" || $ARGV[1] eq "restore" ) && $timeout < 300 ) {
    # make sure we have decent timeout for slow operations 
    $agent->timeout(300);
}

$fbx->uri("${host}/agent/rpc");
if ( $ARGV[1] eq "backup" ) {
    $xml  = RPC::XML::request->new($method, smart_encode(@params));
    $req->content_type('text/xml');
    $req->content($xml->as_string);
    $resp = $agent->request($req);
    $_ = $resp->decoded_content;

    if ( /faultCode/ ) {
        $p = RPC::XML::Parser->new();

        $xml=$p->parse($_);
        if ( ref $xml && $xml->is_fault) {
            printf ("error %d: %s\n",$xml->value->code,$xml->value->string);
        } else {
            # this should not happenned 
            printf ("error: %s\n",$_);
        }

        exit 1;
    }

    if ( $resp->header("Content-type") eq "application/binary") {
        printf ("backup done %d\n",$resp->header("Content-Length"));   
        $data =  $resp->decoded_content;
    } elsif ($resp->header("Content-type") eq "text/xml") {
        printf("response='%s'\n",$resp->decoded_content);
        # strip XML tags */
        # "normal" xml processing was way too slow on large data like backup
        s/<\?xml version=\"1.0\"\?>\n<methodResponse>\n *<params><param><value><base64>//;
        s/<\/base64><\/value><\/param><\/params>\n<\/methodResponse>//;
    
        $data = decode_base64($_);
        {
            use bytes;
            printf ("backup done %d\n",length($data));
        }
    } else {
        printf ("error: %s\n",$_);
        exit 1;
    }

    print FILE $data;
    close(FILE);
    if (! defined $options{s}) {
        $fbx->send_request('/agent/logout',RPC::XML::struct->new(sid=>$sid));
    }
    exit 0;
} 

###################################
#
# UPGRADE
#
# This can be done via XML-RPC as well by posting "image" but it requires more resouce because of base64 and xml encoding
#
if (  $ARGV[1] eq "upgrade" || $ARGV[1] eq "restore" ) {
    my $upgrade =  HTTP::Request->new(POST => "${host}${method}");
    $upgrade->header('Cookie',"sessionid=$sid");
    $upgrade->content_type('application/binary');
    $upgrade->content($image);
   
    if (  $ARGV[1] eq "upgrade" ) {
        $fbx->send_request('/agent/action',RPC::XML::struct->new(action=>"/system/preupdate"));
    }

    my $resp = $agent->request($upgrade);
    $_ = $resp->decoded_content;

    $p = RPC::XML::Parser->new();
    $xml=$p->parse($_);

    if (!ref $xml) {
         printf ("error: %s\n",$_);
        exit 1;
    }

    if (  $ARGV[1] eq "upgrade" ) {
        $fbx->send_request('/agent/action',RPC::XML::struct->new(action=>"/system/postupdate"));
    }

    if ( $xml->is_fault) {
        if ( $xml->value->code eq 101 ) {
            if ($reboot) {
                printf ("reboot needed to complete operation. rebooting now...\n");
                $fbx->send_request('/agent/action',RPC::XML::struct->new(action=>"/system/reboot"));
                exit 0;
            } else {
                printf ("reboot needed to complete operation. reboot not done\n");
                if (! defined $options{s}) {
                    $fbx->send_request('/agent/logout',RPC::XML::struct->new(sid=>$sid));
                }
                exit 0;
            }
        }
        if (! defined $options{s}) {
            $fbx->send_request('/agent/logout',RPC::XML::struct->new(sid=>$sid));
        }
        printf ("error %d: %s\n",$xml->value->code,$xml->value->string);
        exit 1;
    } else {
        print $xml->value->value;
    }
    if (! defined $options{s}) {
        $fbx->send_request('/agent/logout',RPC::XML::struct->new(sid=>$sid));
    }
    exit 0;
}

$resp = $fbx->send_request($method,smart_encode(@params));
if (! defined $options{s}) {
    $fbx->send_request('/agent/logout',RPC::XML::struct->new(sid=>$sid));
}

if (!ref $resp ) {
    printf ("error: $resp\n");
    exit 1;
} elsif ( $resp->is_fault) {
    printf ("error %d: %s\n",$resp->code,$resp->string);
    exit 2
}else {
    printf ("%s", $resp->value);
}
